package com.kgc.scd.controller;

import com.kgc.scd.annotation.RequestPermission;
import com.kgc.scd.annotation.RequestUser;
import com.kgc.scd.bean.Order;
import com.kgc.scd.bean.Seckill;
import com.kgc.scd.service.SeckillBuyFeignService;
import com.kgc.scd.service.SeckillBuyService;
import com.kgc.scd.util.RequestResult;
import com.kgc.scd.util.ResultBuildUtil;
import com.kgc.scd.util.UserAnno;
import io.swagger.annotations.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @program: com.kgc.scd.controller
 * @author: HarryXzy
 * @create: 2022-06-15 14:04
 * @description:
 **/
@Slf4j
@RestController
@Api(tags = "18-2、开始秒杀操作入口")
public class SecKillBuyController {

    @Autowired
    private SeckillBuyService seckillService;

    @Autowired
    private SeckillBuyFeignService seckillBuyFeignService;

    /**
     * @author : 98730
     * @date   : 2022/6/7
     * @param  : []
     * @return : com.kgc.scd.util.RequestResult<java.lang.String>
     * @description : 开始有序秒杀抢购操作入口
     */
    @PostMapping("/seckillBuy")
    @RequestPermission
    @ApiOperation(value = "开始秒杀", notes = "有序秒杀抢购")
    @ApiResponses({
            @ApiResponse(code = 200, message = "SUCCESS"),
            @ApiResponse(code = 500, message = "FAIL")
    })
    public RequestResult<Order> seckillBuy(@RequestUser UserAnno userAnno,
                                            @RequestParam String seckillId){
        // TODO 增加请求许可验证，参考token自定义注解，校验抢购用户权限（必须是已登录授权状态）
        // TODO 增加对请求参数的校验，用户是否正确，商品信息是否正确（根据编号获取商品信息）等等

        log.info("web----------seckillId:{}",seckillId);
        Seckill seckill = seckillBuyFeignService.SelectProIdBySeckillId(seckillId);
        String prodId = seckill.getSeckillProid();

        // 如果瞬时请求用户数过多，可以借助redis的incr原子操作，计数，超出设定请求数量（多出库存数量），直接返回失败（当前请求用户过多）
        if (seckillService.limitBuyUserCount(prodId)){
            return ResultBuildUtil.fail("301","抢购失败，请求用户数量过多!");
        }
        log.info("------ 开始抢购，当前用户：{}，当前商品：{}，购买数量：{}",userAnno.getUserId(),prodId,1);

        // TODO 提高抢购入口的并发处理能力，要减少数据库交互，可以设计为根据商品编号，从redis缓存中查询商品，如果商品信息存在，则参与抢购，如果不存在，还是需要到数据库查询商品，如果数据库中存在，将商品信息存入redis缓存，如果数据库不存在，则直接提示抢购失败
        // TODO 此种场景正常情况没有问题，可能存在的问题：某个商品，是首次参与抢购，缓存中没有数据，但是数据库有，虽然上面的处理方式，可以解决，但是在高并发场景下，同一时刻会有大批量的请求，来秒杀此商品，此时同时去缓存中获取商品数据，没有数据，又同时去数据库查询，就会导致数据库扛不住压力，可能直接数据库崩溃挂掉。
        // TODO 解决方式：缓存商品数据一般都是在后台添加抢购商品时，直接对商品进行预热处理，即：实现把参与抢购的商品直接同步到redis缓存中，这样当抢购开始，直接从redis缓存就可以直接获取到商品，而不会出现缓存击穿问题
        // TODO 虽然商品数据预热方式，可以解决此类问题，但是可能还会存在例外（比如：缓存中的商品由于后台失误操作，导致设置的过期时间不对，缓存时间提前过期，或者缓存数据误删除），此种情况还是需要当缓存不存在商品数据，从数据库查询，放入缓存的逻辑
        // TODO 解决方式：可以进行加锁，限制在高并发的情况下访问数据库，如果同一时刻，缓存中没有获取到商品数据，就进入查询数据库操作，但是在进入查询前，增加分布式锁，只有获取到锁的请求，才可以查询数据库并加入到缓存中（加完就释放锁），获取不到锁的请求，直接返回失败（损失当前请求，后续进行弥补，只要一个操作成功，缓存中的数据就存在，后续的请求自然就可以获取到数据）
        // TODO 极端情况：redis无法使用，必须要增加redis的高可用，确保redis永远是有效的，考虑的模式就是集群下的哨兵机制。或者把大批量的请求直接放到消息队列，进行缓冲处理。（推荐使用哨兵）

        // 获取商品库存数量（模拟将商品库存放入redis中，实际业务中，是在后台预热抢购商品时，同步的）
        seckillService.initProdStock2Redis(prodId);

        // 校验商品库存是否充足，如果库存充足，可以进行后续抢购，如果不足就直接抢购失败
        log.info("------ 1 校验抢购商品：{}，库存是否充足",prodId);

        if (!seckillService.checkProdStockEnough(prodId, 1)) {
            return ResultBuildUtil.fail("302","抢购失败，商品库存不足！");
        }

        // 增加限制处理，锁定当前抢购用户，一个用户同一个商品只能抢一次，加锁，锁整个活动时间
        log.info("------ 2 校验抢购用户：{}，是否已经参与过当前商品的抢购 ------",userAnno.getUserId());
        if (seckillService.checkSeckillUserBought(userAnno.getUserId(),prodId)){
            return ResultBuildUtil.fail("303","抢购失败，不能重复抢购！");
        }

        // 开始进行抢购，给商品库存上锁（上锁成功，可以继续购买，如果上锁失败，说明有人已经在抢购，要继续等待库存锁释放）
        log.info("------ 3 校验库存锁，用户：{}，商品：{}，库存上锁 ------",userAnno.getUserId(),prodId);
        // 使用死循环，等待库存锁释放
        while (true) {
            // 不停的尝试锁库存，如果锁库存成功，代表库存锁已经被释放，可以继续购买，如果锁库存失败，代表有人在抢购，继续等待锁释放
            if (!seckillService.checkProdStockLocked(prodId)) {
                log.info("------ 4 用户：{}，商品：{}，库存上锁成功 ------",userAnno.getUserId(),prodId);
                break;
            }
            log.info("------ 4 用户：{}，商品：{}，库存上锁失败，等待释放 ------",userAnno.getUserId(),prodId);
        }

        // 扣减库存，如果库存扣减失败（并发情况下，多个用户同时库存充足，进入抢购，但是库存只够部分用户购买），返回失败
        if (seckillService.subProdStock(prodId,1)){
            // 库存已经不足，释放库存锁
            seckillService.unlockProdStock(prodId);
            return ResultBuildUtil.fail("302","抢购失败，商品库存不足！");
        }

        // 调用订单中心的业务接口，生成订单，返回
        log.info("------ 5 用户：{}，商品：{}，调用订单接口，开始下单 ------",userAnno.getUserId(),seckillId);
        Order order = seckillService.createOrder(userAnno.getUserId(),seckillId);


        log.info("------ 6 用户：{}，商品：{}，调用订单接口，下单成功，订单号：{} ------",userAnno.getUserId(),prodId,order.getOrderId());

        // 生成订单成功，立刻释放库存锁，其他用户继续抢购
        seckillService.unlockProdStock(prodId);

        // 返回抢购成功
        return ResultBuildUtil.success(order);
    }
}
